假设你要查“比赵琳琳年龄大的学生”,这时数据库并不知道“赵琳琳几岁”。所以必须先查出赵琳琳的年龄,再拿这个年龄去筛选别人。
这种“先查一个小答案,再用这个小答案完成大查询”的方式,就是子查询。
一、先把子查询的骨架看懂
📘 什么是子查询
把一个查询语句写到另一个查询语句内部,里面的查询叫 子查询,外面的查询叫 父查询。
💬 人话翻译
父查询像主任务,子查询像先去打听消息的小助手。小助手把结果带回来,父查询再决定最后要哪些记录。
✅ 两个最关键的判断
第一,看子查询有没有用到父查询当前行的数据。第二,看子查询返回的是一个值,还是一组值。
| 判断点 | 结论 |
|---|---|
| 不依赖父查询当前行 | 普通子查询 |
| 依赖父查询当前行 | 相关子查询 |
| 返回一个值 | 常配 =、>、<> |
| 返回一组值 | 常配 IN、ANY、ALL |
普通子查询和相关子查询,执行顺序完全不同
普通子查询
1
先执行子查询
2
得到一个值或一组值
3
父查询拿结果比较
通常可以理解为:子查询先跑完,再交给外层使用。
相关子查询
1
父查询先取一行
2
子查询读取这行中的值
3
逐行重复判断
可以理解为:父查询每走一行,子查询就要重新问一次。
常见表名快速回忆
学生表 s
| sno | sn | age | maj |
|---|---|---|---|
| s1 | 张三 | 18 | 计算机 |
| s2 | 赵琳琳 | 19 | 数学 |
教师表 t
| tno | tn | prof | sal |
|---|---|---|---|
| t1 | 顾伟 | 副教授 | 5200 |
| t2 | 李明 | 讲师 | 4300 |
课程/选课/授课表
| 表 | 关键字段 |
|---|---|
| c | cno, cn, ct |
| sc | sno, cno, score |
| tc | tno, cno, tcdate |
二、返回一个值的普通子查询
一句话记忆
子查询只返回一个结果时,父查询通常把这个结果当成一个普通常量来比较。
查询比学生“赵琳琳”年龄大的学号、姓名和年龄
模拟数据:学生表 s
| sno | sn | age | maj |
|---|---|---|---|
| s1 | 张三 | 18 | 计算机 |
| s2 | 赵琳琳 | 19 | 数学 |
| s3 | 王敏 | 20 | 计算机 |
| s4 | 陈晨 | 21 | 英语 |
SELECT sno, sn, age
FROM s
WHERE age > (
SELECT age
FROM s
WHERE sn = '赵琳琳'
);1
先在 s 表中查出赵琳琳的 age
2
假设查到结果是 19
3
外层再找 age > 19 的学生
💬 人话翻译
这题不是直接比赵琳琳,而是先把“赵琳琳的年龄”提取出来,再让所有学生和这个年龄比较。
运行结果
| sno | sn | age |
|---|---|---|
| s3 | 王敏 | 20 |
| s4 | 陈晨 | 21 |
查询与教师“顾伟”职称不同的教师号、姓名和职称
模拟数据:教师表 t
| tno | tn | prof | maj | dept | sal |
|---|---|---|---|---|---|
| t1 | 顾伟 | 副教授 | 计算机 | 计算机学院 | 5200 |
| t2 | 李明 | 讲师 | 数学 | 数学学院 | 5600 |
| t3 | 王华 | 教授 | 计算机 | 计算机学院 | 6800 |
| t4 | 刘芳 | 讲师 | 英语 | 外国语学院 | 7000 |
| t5 | 周强 | 副教授 | 计算机 | 计算机学院 | 6100 |
| t6 | 陈刚 | 副教授 | 化学 | 化学学院 | 6300 |
SELECT tno, tn, prof
FROM t
WHERE prof <> (
SELECT prof
FROM t
WHERE tn = '顾伟'
);理解提醒
<> 表示“不等于”。子查询先查出顾伟的职称,外层再筛选不同职称的教师。
运行结果
| tno | tn | prof |
|---|---|---|
| t2 | 李明 | 讲师 |
| t3 | 王华 | 教授 |
| t4 | 刘芳 | 讲师 |
查询讲授“程序设计基础”课程的教师号、课程号和开课日期
模拟数据:课程表 c
| cno | cn | ct |
|---|---|---|
| c1 | 程序设计基础 | 64 |
| c2 | 数据库原理 | 48 |
| c4 | Web前端开发 | 64 |
模拟数据:授课表 tc
| tno | cno | tcdate |
|---|---|---|
| t1 | c1 | 2024-09-01 |
| t1 | c2 | 2024-09-01 |
| t2 | c4 | 2024-09-10 |
| t3 | c1 | 2025-02-20 |
SELECT tno, cno, tcdate
FROM tc
WHERE cno = (
SELECT cno
FROM c
WHERE cn = '程序设计基础'
);容易卡住的点
课程名在 c 表里,授课信息在 tc 表里。题目想找的是授课信息,所以必须先从课程表查出课程号,再去授课表里找。
运行结果
| tno | cno | tcdate |
|---|---|---|
| t1 | c1 | 2024-09-01 |
| t3 | c1 | 2025-02-20 |
这一类题的统一思路
| 步骤 | 理解思路 |
|---|---|
| 第 1 步 | 先找题目中的“参照对象”,例如赵琳琳、顾伟、程序设计基础 |
| 第 2 步 | 用子查询查出这个参照对象对应的一个具体值,例如年龄、职称、课程号 |
| 第 3 步 | 让父查询拿着这个具体值去做比较或匹配 |
三、返回一组值的普通子查询
先把三个关键词记住
| 写法 | 大意 | 直观理解 |
|---|---|---|
| IN | 在这组值里面 | 像“名单里有没有他” |
| = ANY | 等于其中任意一个 | 效果和 IN 很接近 |
| > ANY | 只要大于其中某一个就行 | 本质上等于大于最小值 |
| > ALL | 必须大于全部 | 本质上等于大于最大值 |
✅ 实战记忆
= ANY 基本可以记成“等于这个集合里的任意一个值”,所以在很多题里和 IN 作用相同。
💬 人话翻译
单值子查询像“问一个答案”,多值子查询像“拿到一份名单”。
查询学号为 s2 的学生选修的课程号、课程名和课时
模拟数据:课程表 c
| cno | cn | ct |
|---|---|---|
| c1 | 程序设计基础 | 64 |
| c2 | 数据库原理 | 48 |
| c3 | 高等数学 | 56 |
| c4 | Web前端开发 | 64 |
模拟数据:选课表 sc
| sno | cno | score |
|---|---|---|
| s1 | c1 | 86 |
| s1 | c2 | 90 |
| s2 | c1 | 88 |
| s2 | c2 | 92 |
| s3 | c3 | 85 |
| s4 | c2 | 69 |
SELECT cno, cn, ct
FROM c
WHERE cno = ANY (
SELECT cno
FROM sc
WHERE sno = 's2'
);SELECT cno, cn, ct
FROM c
WHERE cno IN (
SELECT cno
FROM sc
WHERE sno = 's2'
);结论
这两题任务相同,= ANY 和 IN 在这里作用相同,都表示“课程号属于 s2 选过的课程号集合”。
运行结果
| cno | cn | ct |
|---|---|---|
| c1 | 程序设计基础 | 64 |
| c2 | 数据库原理 | 48 |
查询其他专业中比“计算机”专业某一教师工资高的教师
模拟数据:教师表 t
| tno | tn | prof | maj | dept | sal |
|---|---|---|---|---|---|
| t1 | 顾伟 | 副教授 | 计算机 | 计算机学院 | 5200 |
| t2 | 李明 | 讲师 | 数学 | 数学学院 | 5600 |
| t3 | 王华 | 教授 | 计算机 | 计算机学院 | 6800 |
| t4 | 刘芳 | 讲师 | 英语 | 外国语学院 | 7000 |
| t5 | 周强 | 副教授 | 计算机 | 计算机学院 | 6100 |
| t6 | 陈刚 | 副教授 | 化学 | 化学学院 | 6300 |
SELECT tno, tn, maj, sal
FROM t
WHERE (sal > ANY (
SELECT sal
FROM t
WHERE maj = '计算机'
))
AND maj <> '计算机';如何理解 > ANY
“比某一教师工资高”意思不是比所有人都高,只要比其中任意一个高就行。因此等价于:大于计算机专业教师工资中的最小值。
运行结果
| tno | tn | maj | sal |
|---|---|---|---|
| t2 | 李明 | 数学 | 5600 |
| t4 | 刘芳 | 英语 | 7000 |
| t6 | 陈刚 | 化学 | 6300 |
SELECT tno, tn, maj, sal
FROM t
WHERE sal > (
SELECT MIN(sal)
FROM t
WHERE maj = '计算机'
)
AND maj <> '计算机';查询选修课程号为 c1 的学号和姓名
模拟数据:学生表 s
| sno | sn | age | maj |
|---|---|---|---|
| s1 | 张三 | 18 | 计算机 |
| s2 | 赵琳琳 | 19 | 数学 |
| s3 | 王敏 | 20 | 计算机 |
| s4 | 陈晨 | 21 | 英语 |
模拟数据:选课表 sc
| sno | cno | score |
|---|---|---|
| s1 | c1 | 86 |
| s1 | c2 | 90 |
| s2 | c1 | 88 |
| s2 | c2 | 92 |
| s3 | c3 | 85 |
| s4 | c2 | 69 |
SELECT sno, sn
FROM s
WHERE sno IN (
SELECT sno
FROM sc
WHERE cno = 'c1'
);💬 人话翻译
先在选课表里查出谁选了 c1,再去学生表里把这些人的姓名取出来。
运行结果
| sno | sn |
|---|---|
| s1 | 张三 |
| s2 | 赵琳琳 |
查询其他专业中比“计算机”专业所有教师工资都高的教师
模拟数据:教师表 t
| tno | tn | prof | maj | dept | sal |
|---|---|---|---|---|---|
| t1 | 顾伟 | 副教授 | 计算机 | 计算机学院 | 5200 |
| t2 | 李明 | 讲师 | 数学 | 数学学院 | 5600 |
| t3 | 王华 | 教授 | 计算机 | 计算机学院 | 6800 |
| t4 | 刘芳 | 讲师 | 英语 | 外国语学院 | 7000 |
| t5 | 周强 | 副教授 | 计算机 | 计算机学院 | 6100 |
| t6 | 陈刚 | 副教授 | 化学 | 化学学院 | 6300 |
SELECT tno, tn, maj, sal
FROM t
WHERE (sal > ALL (
SELECT sal
FROM t
WHERE maj = '计算机'
))
AND maj <> '计算机';这一题和上题的区别一定要分清
> ANY 是只要比其中某一个高。> ALL 是必须比所有人都高。
所以 sal > ALL(...) 可以理解为:工资必须大于这个集合中的最大值。
运行结果
| tno | tn | maj | sal |
|---|---|---|---|
| t4 | 刘芳 | 英语 | 7000 |
SELECT tno, tn, maj, sal
FROM t
WHERE sal > (
SELECT MAX(sal)
FROM t
WHERE maj = '计算机'
)
AND maj <> '计算机';四、子查询不只用于 SELECT,也能用于数据操纵
教材里的第三类场景
子查询还可以出现在 INSERT、UPDATE 这类数据操纵语句中。
把各学院教师的平均工资存入新表 avgsal
模拟数据:教师表 t
| tno | tn | prof | maj | dept | sal |
|---|---|---|---|---|---|
| t1 | 顾伟 | 副教授 | 计算机 | 计算机学院 | 5200 |
| t2 | 李明 | 讲师 | 数学 | 数学学院 | 5600 |
| t3 | 王华 | 教授 | 计算机 | 计算机学院 | 6800 |
| t4 | 刘芳 | 讲师 | 英语 | 外国语学院 | 7000 |
| t5 | 周强 | 副教授 | 计算机 | 计算机学院 | 6100 |
| t6 | 陈刚 | 副教授 | 化学 | 化学学院 | 6300 |
DROP TABLE IF EXISTS avgsal;
CREATE TABLE avgsal (
department VARCHAR(20),
average SMALLINT
);
INSERT INTO avgsal
SELECT dept, AVG(sal)
FROM t
GROUP BY dept;理解提醒
这里教材标题写的是“利用子查询”,但从 SQL 写法看,核心其实是 INSERT INTO ... SELECT ...,也就是把查询结果直接插入新表。
插入后的 avgsal 表
| department | average |
|---|---|
| 计算机学院 | 6033 |
| 数学学院 | 5600 |
| 外国语学院 | 7000 |
| 化学学院 | 6300 |
把教师号为 t1 的教师讲授课程的课时增加 16 学时
模拟数据:课程表 c
| cno | cn | ct |
|---|---|---|
| c1 | 程序设计基础 | 64 |
| c2 | 数据库原理 | 48 |
| c4 | Web前端开发 | 64 |
模拟数据:授课表 tc
| tno | cno | tcdate |
|---|---|---|
| t1 | c1 | 2024-09-01 |
| t1 | c2 | 2024-09-01 |
| t2 | c4 | 2024-09-10 |
| t3 | c1 | 2025-02-20 |
UPDATE c
SET ct = ct + 16
WHERE cno IN (
SELECT cno
FROM tc
WHERE tno = 't1'
);💬 人话翻译
先从授课表 tc 里查出 t1 教了哪些课,再回到课程表 c,把这些课的课时统一加 16。
更新后的课程表 c(只展示受影响记录)
| cno | cn | 原课时 | 新课时 |
|---|---|---|---|
| c1 | 程序设计基础 | 64 | 80 |
| c2 | 数据库原理 | 48 | 64 |
把所有教师工资提高到平均工资的 1.2 倍
模拟数据:教师表 t
| tno | tn | prof | maj | dept | sal |
|---|---|---|---|---|---|
| t1 | 顾伟 | 副教授 | 计算机 | 计算机学院 | 5200 |
| t2 | 李明 | 讲师 | 数学 | 数学学院 | 5600 |
| t3 | 王华 | 教授 | 计算机 | 计算机学院 | 6800 |
| t4 | 刘芳 | 讲师 | 英语 | 外国语学院 | 7000 |
| t5 | 周强 | 副教授 | 计算机 | 计算机学院 | 6100 |
| t6 | 陈刚 | 副教授 | 化学 | 化学学院 | 6300 |
UPDATE t
SET sal = (
SELECT 1.2 * AVG(sal)
FROM t
);理解提醒
子查询只返回一个值:平均工资的 1.2 倍。然后把这个同一个值赋给每一位教师,所以最后所有教师工资会变成一样。
更新后的教师工资
| 说明 | 值 |
|---|---|
| 平均工资 | 6166.67 |
| 1.2 × 平均工资 | 7400 |
| 更新后每位教师工资 | 7400 |
六、EXISTS / NOT EXISTS:不看返回什么值,只看“有没有记录”
核心理解
EXISTS 关心的不是子查询具体返回哪一列,而是子查询结果集是否为空。有记录,就为真。没有记录,就为假。
查询选修了课程号为 c1 的学号和姓名(使用 EXISTS)
模拟数据:学生表 s
| sno | sn | age | maj |
|---|---|---|---|
| s1 | 张三 | 18 | 计算机 |
| s2 | 赵琳琳 | 19 | 数学 |
| s3 | 王敏 | 20 | 计算机 |
| s4 | 陈晨 | 21 | 英语 |
模拟数据:选课表 sc
| sno | cno | score |
|---|---|---|
| s1 | c1 | 86 |
| s1 | c2 | 90 |
| s2 | c1 | 88 |
| s2 | c2 | 92 |
| s3 | c3 | 85 |
| s4 | c2 | 69 |
SELECT sno, sn
FROM s
WHERE EXISTS (
SELECT *
FROM sc
WHERE sno = s.sno AND cno = 'c1'
);理解方式
对每个学生来说,只要在 sc 表里找到一条“这个学生选了 c1”的记录,EXISTS 就成立,这个学生就会被选出来。
运行结果
| sno | sn |
|---|---|
| s1 | 张三 |
| s2 | 赵琳琳 |
查询没有选修课程号为 c1 的学号和姓名(使用 NOT EXISTS)
模拟数据:学生表 s
| sno | sn | age | maj |
|---|---|---|---|
| s1 | 张三 | 18 | 计算机 |
| s2 | 赵琳琳 | 19 | 数学 |
| s3 | 王敏 | 20 | 计算机 |
| s4 | 陈晨 | 21 | 英语 |
模拟数据:选课表 sc
| sno | cno | score |
|---|---|---|
| s1 | c1 | 86 |
| s1 | c2 | 90 |
| s2 | c1 | 88 |
| s2 | c2 | 92 |
| s3 | c3 | 85 |
| s4 | c2 | 69 |
SELECT sno, sn
FROM s
WHERE NOT EXISTS (
SELECT *
FROM sc
WHERE sno = s.sno AND cno = 'c1'
);💬 人话翻译
只要找不到“该学生选了 c1”的记录,NOT EXISTS 就为真。
运行结果
| sno | sn |
|---|---|
| s3 | 王敏 |
| s4 | 陈晨 |
查询教师号为 t2 的教师讲授的课程号、课程名和课时
模拟数据:课程表 c
| cno | cn | ct |
|---|---|---|
| c1 | 程序设计基础 | 64 |
| c2 | 数据库原理 | 48 |
| c4 | Web前端开发 | 64 |
模拟数据:授课表 tc
| tno | cno | tcdate |
|---|---|---|
| t1 | c1 | 2024-09-01 |
| t1 | c2 | 2024-09-01 |
| t2 | c4 | 2024-09-10 |
| t3 | c1 | 2025-02-20 |
SELECT cno, cn, ct
FROM c
WHERE EXISTS (
SELECT *
FROM tc
WHERE cno = c.cno AND tno = 't2'
);为什么外层是课程表 c
因为最终想输出的是课程号、课程名和课时,这些字段都在课程表 c 中。EXISTS 只是帮我们判断:当前这门课是不是 t2 教过的。
运行结果
| cno | cn | ct |
|---|---|---|
| c4 | Web前端开发 | 64 |
七、练习
练习 1
下面哪一句最能说明“相关子查询”的特点?
A. 子查询一定只返回一个值
B. 子查询会用到父查询当前行中的字段值
C. 子查询只能写在 SELECT 后面
D. 子查询执行完后一定不会再执行
正确答案是 B。相关子查询的本质,是子查询条件依赖父查询当前处理的这一行,所以会逐行执行。
练习 2
如果子查询返回的是一组课程号,外层常用哪个运算符判断“某课程号是否在这组结果中”?
A. <>
B. >
C. IN
D. EXISTS
正确答案是 C。IN 最适合“属于某个集合”这种判断。
练习 3
sal > ALL (子查询) 最接近下面哪种理解?
A. 只要大于其中一个值
B. 等于其中任意一个值
C. 在这个集合里面
D. 大于这个集合中的最大值
正确答案是 D。要比集合中的所有值都大,本质上就必须大于这个集合的最大值。